package asteroids.model;

import java.util.Random;

import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Immutable;
import be.kuleuven.cs.som.annotate.Model;

import ModelExceptions.IllegalMassException;
import ModelExceptions.IllegalRadiusException;
import ModelExceptions.OutOfBoundsException;
import asteroids.Util;

/**
 * 
 * @author Thomas Coenaerts and Carsten Bakker
 *
 */
public class Asteroid extends CollisionObject{


	/**
	 * 
	 * @param xCoordinate
	 * @param yCoordinate
	 * @param xVelocity
	 * @param yVelocity
	 * @param radius
	 * @param density
	 * @post	...
	 * 			|if(isValidDensity(density) then
	 * 			|		new.getDensity() == density
	 * @post	...
	 * 			|if(!isValidDensity(density) then
	 * 			|		new.getDensity() == getDefaultDensity()
	 * @post	...
	 * 			|new.getMass() = setMass(radius)
	 * @invar	...
	 * 			|isValidDensity(getDensity())
	 */
	public Asteroid (double xCoordinate, double yCoordinate, double xVelocity, double yVelocity, double radius, double density){
		super(xCoordinate, yCoordinate, xVelocity, yVelocity, radius);
		if(isValidDensity(density))
			this.density = density;
		else
			this.density = this.getDefaultDensity();
	}


	/**
	 * Creates a default asteroid with default density
	 * @param xCoordinate
	 * @param yCoordinate
	 * @param xVelocity
	 * @param yVelocity
	 * @param radius
	 * @throws OutOfBoundsException
	 * @throws IllegalRadiusException
	 * @post	...
	 * 			| new.getDensity() = getDefaultDensity()
	 * @post	...
	 * 			| new.getMass() = setMass(radius)
	 */
	public Asteroid(double xCoordinate, double yCoordinate, double xVelocity,double yVelocity, double radius)//, double angle, double mass)
			throws OutOfBoundsException, IllegalRadiusException {
		super(xCoordinate, yCoordinate, xVelocity, yVelocity, radius);
		this.density =  getDefaultDensity();
		setMass(radius);
	}



	/**
	 * 
	 * @param xCoordinate
	 * @param yCoordinate
	 * @param xVelocity
	 * @param yVelocity
	 * @param radius
	 * @param random
	 * @effect	...
	 * 			|new Asteroid((double xCoordinate, double yCoordinate, double xVelocity, double yVelocity, double radius)
	 * @post	...
	 * 			|new.getRandomgenerator = random;
	 */
	public Asteroid (double xCoordinate, double yCoordinate, double xVelocity, double yVelocity, double radius, Random random){
		super(xCoordinate, yCoordinate, xVelocity, yVelocity, radius);
		setRandomGenerator(random);
		this.density= getDefaultDensity();
		setMass(radius);
	}
	
	/**
	 * 
	 * @return The randomgenerator from this asteroid. As an example: to get a random direction of child asteroids
	 */
	@Model
	private Random getRandomGenerator(){
		return this.randomGenerator;
	}
	
	private void setRandomGenerator(Random random){
		this.randomGenerator = random;
	}
	private Random randomGenerator = new Random();



	 
	/**
	 * @param radius
	 * @post 	...
	 * 			| (new this).getMass() == Math.pow(radius, 3)*4/3*Math.PI*getDensity()
	 * @throws IllegalMassException	!isValidMass(new.getMass())
	 */
	@Override
	public void setMass(double radius) throws IllegalMassException{
		double calcMass = Math.pow(radius, 3)*4/3*Math.PI*getDensity();
		if(isValidMass(calcMass))
			this.mass = calcMass;
		else 
			throw new IllegalMassException("The density and radius result in an invalid mass");
	}

	/**
	 * Get the density from the collisionObject;
	 * @return The density of the collisionobject
	 */
	@Basic
	private double getDensity(){
		return this.density;
	}

	/**
	 * 
	 * @param density 	The density you want to check
	 * @return		...	
	 * 				|if(Util.fuzzyLessThanOrEqualTo(density, getMinimumDensity()) 
	 * 				|			|| !Util.fuzzyLessThanOrEqualTo(density, getMaximumDensity())
	 * 				|			|| Double.isNaN(density)) 						then
	 *				|result == true;
	 */
	private boolean isValidDensity(double density) {
		if(Util.fuzzyLessThanOrEqualTo(density, getMinimumDensity()) || !Util.fuzzyLessThanOrEqualTo(density, getMaximumDensity())|| Double.isNaN(density))
			return true;
		else return false;
	}

	@Basic
	private double getMaximumDensity(){
		return this.maximumDensity;
	}
	@Basic
	private double getMinimumDensity(){
		return this.minimumDensity;
	}
	@Basic
	private double getDefaultDensity(){
		return this.defaultDensity;
	}
	private final double defaultDensity 	= 	2.65*Math.pow(10, 12);
	private final double minimumDensity 	=	0.0;
	private final double maximumDensity 	= 	Double.MAX_VALUE;
	private final double density;

	/**
	 * 
	 * @param collisionObject
	 * @post	...
	 * 			| if(collisionObject.isBullet()) 	then
	 *			|	new.isDestroyed() && collisionObject.isDestroyed()
	 * @effect	...
	 * 			| if(collisionObject.isAsteroid()) 	then
	 *			|	this.bounceOffObject(collisionObject);
	 * @post	...
	 * 			|if(collisionObject.isShip())		then
	 *			| 	collisionObject.isDestroyed();	
	 *
	 */
	@Override
	public void collisionReactionWith(CollisionObject collisionObject) {
		if(collisionObject.isAsteroid()){
			this.bounceOffObject(collisionObject);
			//collisionObject.bounceOffObject(this);
		}
		if(collisionObject.isBullet()){
			this.destroy();
			collisionObject.destroy();			
		}
		if(collisionObject.isShip()){
			collisionObject.destroy();
		}
	}

	/**
	 * @post...
	 * 		|if(!Util.fuzzyEquals(getRadius(),getMinimumSizeForSplittin()) && Util.fuzzyLessThanOrEqualTo(this.getRadius(), 30) ) {				//If it's strictly smaller than minimumsizeforsplitting -> destroy completely
	 *		|completeDestruction();
	 *		|}
	 *@post |if(Util.fuzzyEquals(getRadius(),getMinimumSizeForSplittin()) && !Util.fuzzyLessThanOrEqualTo(this.getRadius(), 30) ) {
	 *		|newXVelocity = getChildXVelocity(angle, totalVelocity);
	 *		|newYVelocity = getChildYVelocity(angle, totalVelocity);
	 *		|Asteroid child1 = new Asteroid(this.getFirstChildXCoordinate(angle),this.getFirstChildYCoordinate(angle), newXVelocity, newYVelocity, getChildRadius());
	 *		|Asteroid child2 = new Asteroid(this.getSecondChildXCoordinate(angle), this.getSecondChildYCoordinate(angle), -newXVelocity, -newYVelocity, getChildRadius());
	 *		|child1.setWorld(this.getWorld());
	 *		|child2.setWorld(this.getWorld());
	 *		|if(child1.exceedsWorldBoundaries()){
	 *		|	child1.completeDestruction();
	 *		|}
	 *		|if(child2.exceedsWorldBoundaries()){
	 *		|	child2.completeDestruction();
	 *		|}
	 *		|this.completeDestruction();
	 */
	/**
	 * @post	...
	 * 			| new.isDestroyed()
	 * @effect	...
	 * 			| super.destroy()
	 * @effect	...	
	 * 			| if(Util.fuzzyEquals(getRadius(),getMinimumSizeForSplittin()) 
	 * 			|			&& !Util.fuzzyLessThanOrEqualTo(this.getRadius(), 30) ) then
	 * 			|
	 * 			| 	newXVelocity 	= 	getChildXVelocity(angle, totalVelocity);
	 *			|	newYVelocity 	= 	getChildYVelocity(angle, totalVelocity);
	 *			|	Asteroid child1 = 	new Asteroid(this.getFirstChildXCoordinate(angle),this.getFirstChildYCoordinate(angle), newXVelocity, newYVelocity, getChildRadius());
	 *			|	Asteroid child2 = 	new Asteroid(this.getSecondChildXCoordinate(angle), this.getSecondChildYCoordinate(angle), -newXVelocity, -newYVelocity, getChildRadius());
	 *			|	child1.setWorld(this.getWorld());
	 *			|	child2.setWorld(this.getWorld());
	 *			|	if(child1.exceedsWorldBoundaries()) then
	 *			|		child1.completeDestruction();
	 *			|	
	 *			|	if(child2.exceedsWorldBoundaries()) then
	 *			|		child2.completeDestruction();
	 *			|	
	 */
	public void destroy(){

		if(!Util.fuzzyEquals(getRadius(),getMinimumSizeForSplittin()) && Util.fuzzyLessThanOrEqualTo(this.getRadius(), 30) ) {				//If it's strictly smaller than minimumsizeforsplitting -> destroy completely
			completeDestruction();
		}
		else{			//if it's greater than or equal to the minimumsizeforsplitting-> split
			int random = getRandomGenerator().nextInt(360);
			double angle = Math.toRadians(random);
			double totalVelocity =this.getTotalVelocity(getXVelocity(), getYVelocity());
			double newXVelocity;
			double newYVelocity;
			// OF isValidVelocity(getChildVelocityMultiplicator()*totalVelocity)) // maar als dan op andere manier total velocity berekend wordt.. Kan evt vermeden worden door dit in de documentatie te schrijven.
			if(isValidVelocity(getChildXVelocity(angle, totalVelocity), getChildYVelocity(angle, totalVelocity))){
				newXVelocity = getChildXVelocity(angle, totalVelocity);
				newYVelocity = getChildYVelocity(angle, totalVelocity);
			}
			else{
				newXVelocity = getSpeedOfLight()*Math.cos(angle);
				newYVelocity = getSpeedOfLight()*Math.sin(angle);

			}
			Asteroid child1 = new Asteroid(this.getFirstChildXCoordinate(angle),this.getFirstChildYCoordinate(angle), newXVelocity, newYVelocity, getChildRadius());
			Asteroid child2 = new Asteroid(this.getSecondChildXCoordinate(angle), this.getSecondChildYCoordinate(angle), -newXVelocity, -newYVelocity, getChildRadius());
			child1.setWorld(this.getWorld());
			child2.setWorld(this.getWorld());
			if(child1.exceedsWorldBoundaries()){
				child1.completeDestruction();
			}
			if(child2.exceedsWorldBoundaries()){
				child2.completeDestruction();
			}
			this.completeDestruction();
		}
	}


	private void completeDestruction(){
		super.destroy();
	}

	/**
	 * 
	 * @throws 	IllegalRadiusException
	 * @return 	...
	 * 			| result= this.getRadius()/2;
	 * 			| return result;
	 * @throws 	...
	 * 			|!isValidRadius(result)
	 */
	public double getChildRadius() throws IllegalRadiusException{
		double newRadius = this.getRadius()/2;
		if(this.isValidRadius(newRadius))
			return newRadius;
		throw new IllegalRadiusException("Invalid childradius");
	}
	/**
	 * @param angle			The direction in which the asteroid is going
	 * @param totalVelocity	The total velocity(X and Y combined) 
	 * @return	...
	 * 			|result == getChildVelocityMultiplicator()*totalVelocity*Math.cos(angle); 
	 */
	public double getChildXVelocity(double angle, double totalVelocity){
		return getChildVelocityMultiplicator()*totalVelocity*Math.cos(angle);
	}
	/**
	 * 
	 * @param angle			The direction in which the asteroid is going
	 * @param totalVelocity	The total velocity(X and Y combined) 
	 * @return	...
	 * 			|result == getChildVelocityMultiplicator()*totalVelocity*Math.sin(angle); 
	 */
	public double getChildYVelocity(double angle, double totalVelocity){
		return getChildVelocityMultiplicator()*totalVelocity*Math.sin(angle);
	}
	/**
	 * 
	 * @param angle	The direction in which the asteroid is going
	 * @return		...
	 * 				|double xRadius = (this.getRadius()/2)*Math.cos(angle);
	 *				|double xCoordinate = this.getXCoordinate();
	 *				|result == 	xRadius +xCoordinate;
	 *				|return result
	 */
	private double getFirstChildXCoordinate(double angle){
		double xRadius = (this.getRadius()/2)*Math.cos(angle);
		double xCoordinate = this.getXCoordinate();
		return 	(xRadius +xCoordinate);
	}
	/**
	 * @param angle	The direction in which the asteroid is going
	 * @return		...
	 * 				|double yRadius =(this.getRadius()/2)*Math.sin(angle);
	 *				|double yCoordinate =this.getYCoordinate();
	 *				|result ==	yCoordinate	+ yRadius;
	 *				|return result;
	 */
	private double getFirstChildYCoordinate(double angle){
		double yRadius =(this.getRadius()/2)*Math.sin(angle);
		double yCoordinate =this.getYCoordinate();
		return	(yCoordinate	+ yRadius);
	}
	/**
	 * 
	 * @param angle	The direction in which the asteroid is going
	 * @return		...
	 * 				|double xRadius = (this.getRadius()/2)*Math.cos(angle+Math.PI);
	 *				|double xCoordinate = this.getXCoordinate();
	 *				|return 	xRadius +xCoordinate;
	 */
	public double getSecondChildXCoordinate(double angle){
		double xRadius = (this.getRadius()/2)*Math.cos(angle+Math.PI);
		double xCoordinate = this.getXCoordinate();
		return 	xRadius +xCoordinate;
	}
	/**
	 * 
	 * @param angle	The direction in which the asteroid is going
	 * @return		|double yRadius =(this.getRadius()/2)*Math.sin(angle+Math.PI);
	 *				|double yCoordinate =this.getYCoordinate();
	 *				| return	yCoordinate	+ yRadius;
	 */
	public double getSecondChildYCoordinate(double angle){
		double yRadius =(this.getRadius()/2)*Math.sin(angle+Math.PI);
		double yCoordinate =this.getYCoordinate();
		return	yCoordinate	+ yRadius;
	}

	@Immutable @Basic
	public double getChildVelocityMultiplicator(){
		return this.childVelocityMultiplicator;
	}

	private final double childVelocityMultiplicator = 1.5;

	@Immutable	@Basic
	public double getMinimumSizeForSplittin(){
		return this.minimumSizeForSplitting;
	}

	private final double minimumSizeForSplitting = 30;

	/**
	 * @return 	...
	 * 			| result == "This is an asteroid with " + super.toString();
	 */
	@Override
	public String toString(){
		return "This is an asteroid with" //\n" 
				+ super.toString();
	}

	/**
	 * @return	...
	 * 			| result.getXCoordinate()	== 	this.getXCoordinate()
	 * 			| result.getYCoordinate()	== 	this.getYCoordinate()
	 * 			| result.getXVelocity()		==	this.getXVelocity()
	 * 			| result.getYVelocity()		== 	this.getYVelocity()
	 * 			| result.getRadius()		==	this.getRadius()
	 * 			| result.getDensity()		== 	this.getDensity()
	 * 			| result.getWorld()			==	this.getWorld()
	 */
	@Override
	public CollisionObject clone(){
		Asteroid asteroid = new Asteroid(getXCoordinate(), getYCoordinate(), getXVelocity(), getYVelocity(), getRadius(), getDensity());
		asteroid.setWorld(this.getWorld());
		return asteroid;
	}

}
